package edu.mtu.citizenscience.cs.utils.gps;

import edu.mtu.citizenscience.cs.utils.Services;
import edu.mtu.citizenscience.cs.utils.services.StopHandler;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
/**
 * @author Corbin Uselton <corbinu@cwuselto@mtu.edu>
 * @copyright (c) 2012 MichiganTech <http://www.mtu.edu>
 * @since 1.0
 * 
 * GPS service which runs in the background behind an
 * app and gets GPS location updates
 */
public class CitsciGPSService extends Service {
	// reference var for setting the update rate
	public static final String SET_UPDATE_RATE = "SET_UPDATE_RATE";
	// reference var for setting the update rate to tight
	public static final String TIGHT = "TIGHT";
	// reference var for setting the update rate to loose
	public static final String LOOSE = "LOOSE";
	
	// reference var for passing the location provider
	public static final String PROVIDER = "PROVIDER";
	// reference var for passing the location latitude
	public static final String LATITUDE = "LATITUDE";
	// reference var for passing the location longitude
	public static final String LONGITUDE = "LOGITUDE";
	// reference var for passing the location altitude
	public static final String ALTITUDE = "ALTITUDE";
	// reference var for passing the location bearing
	public static final String BEARING = "BEARING";
	// reference var for passing the location speed
	public static final String SPEED = "SPEED";
	// reference var for passing the location time
	public static final String TIME = "TIME";
	// reference var for passing the location accuracy
	public static final String ACCURACY = "ACCURACY";
	
	// the GPS update rate defaults to LOOSE
	private static String UPDATE_RATE = "LOOSE";
	
	// minimum GPS update time for tight setting
	private static int TIGHT_TIME = 5 * 1000;
	// minimum GPS update time for loose setting
	private static int LOOSE_TIME = 2 * 60 * 1000;
	// minimum distance for GPS update
	private static int MIN_DISTANCE = 0;
	// minimum accuracy for GPS update
	private static int MIN_ACCURACY = 25;
	// minimum accuracy change for GPS update
	private static int ACCURACY_DELTA = 10;
	
	// manages updates from the android location systems in this case the GPS
	private LocationManager locationManager;
	
	// the most recent location update
	private Location currentLocation;
	
	// stops the service after a set amount of time
	private StopHandler stopHandler = new StopHandler(this);
	// Used to receive messages from the managing CitsciGPS object
	final Messenger inMessenger = new Messenger(new IncomingHandler());
	// Used to send message to the managing CitsciGPS object
	private Messenger outMessenger;
	
	/**
	 * @since 1.0
	 * 
	 * Handles messages from the managing CitsciGPS object
	 */
	@SuppressLint("HandlerLeak")
	class IncomingHandler extends Handler 
	{
	
		/**
		 * Called when a message from the managing CitsciGPS object is received
		 * 
		 * @param   message - Message - The message from the service
		 * @return	void
		 */
		@Override
	    public void handleMessage(Message message) 
	    {
			// get a new update rate from the message data
      		String updateRate = message.getData().getString(SET_UPDATE_RATE);
      		// check if got new update rate from the message
      		if (updateRate != null)
      		{
      			// set the new update rate
      			UPDATE_RATE = updateRate;
      			
      			// remove location updates
      			locationManager.removeUpdates(locationListener);
      			
      			// check if the update rate is tight
      			if (UPDATE_RATE == TIGHT)
      	        {
      				// request new updates in tight time
      	        	locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, TIGHT_TIME, MIN_DISTANCE, locationListener);
      	        }
      	        else
      	        {
      	        	// request new updates in loose time
      	        	locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOOSE_TIME, MIN_DISTANCE, locationListener);
      	        }
      		}
      		
      		// check if already have a location
      		if (currentLocation != null) 
    		{
      			// check if the update rate is loose the the time elapsed between the current time and time of the location is less then loose time
    			if (UPDATE_RATE == LOOSE && (System.currentTimeMillis() - currentLocation.getTime()) < (LOOSE_TIME))
	    		{
    				// send the current location to the managing Citsci GPS object
	    			sendLocation();
	    		}
    			// check if the update rate is tigh the the time elapsed between the current time and time of the location is less then tight time
	    		else if ( (System.currentTimeMillis() - currentLocation.getTime()) < (TIGHT_TIME) )
	    		{
	    			// send the current location to the managing Citsci GPS object
	    			sendLocation();
	    		}
    		}
      		else
      		{
      			// send message with result canceled as do not have the location at the moment
      			Services.sendMessage("GPS", outMessenger, Activity.RESULT_CANCELED, new Bundle());
      		}
		}
	}
	
	/**
	 * @since 1.0
	 * 
	 * Handles location updates from the LocationManager
	 */
	protected LocationListener locationListener = new LocationListener() 
	{
		/**
		 * Called when the location has changed.
		 * 
		 * @param   location - Location - The new location, as a Location object.
		 * @return	void
		 */
		@Override
		public void onLocationChanged(Location location) 
		{
			if (location.getAccuracy() < MIN_ACCURACY)
			{
				locationUpdate(location);
			}
		}

		/**
		 * Called when the provider is disabled by the user. 
		 * If requestLocationUpdates is called on an already disabled provider, 
		 * this method is called immediately.
		 * 
		 * @param   provider - String - The location provider that was disabled
		 * @return	void
		 */
		@Override
		public void onProviderDisabled(String provider) {  }

		/**
		 * Called when the provider is enabled by the user.
		 * 
		 * @param   provider - String - the name of the location provider associated with this update.
		 * @return	void
		 */
		@Override
		public void onProviderEnabled(String provider) {  }

		/**
		 * Called when the provider status changes. This method is called when a 
		 * provider is unable to fetch a location or if the provider has recently 
		 * become available after a period of unavailability.
		 * 
		 * @param   provider - String - the name of the location provider associated with this update.
		 * @param	status - int - AVAILABLE, OUT_OF_SERVICE, or TEMPORARILY_UNAVAILABLE
		 * @param	extras - Bundle - an optional Bundle which will contain provider specific status variables.
		 * @return	void
		 */
		@Override
		public void onStatusChanged(String provider, int status, Bundle extras) {  }
		
	};
	
	/**
	 * @since 1.0
	 * 
	 * Handles GPS status updates from the LocationManager
	 */
	private GpsStatus.Listener statusListener = new GpsStatus.Listener() 
	{

		/**
		 * Called when the GPS status has changed.
		 * 
		 * @param   event - int - Called to report changes in the GPS status.
		 * @return	void
		 */
		@Override
		public void onGpsStatusChanged(int event) 
		{
			// Get the current GPS status
			GpsStatus status = locationManager.getGpsStatus(null);
			
			// get all of the satellites from the GPS Status
			Iterable<GpsSatellite> sats = status.getSatellites();
			// set number of fix satellites to 0
			int nFixed = 0;  
			// set total number of satellites to 0
			int nSats = 0;
			// loop over the satellites
			for (GpsSatellite sat : sats)
			{
				// add to the number of satellites for each satellite
				nSats++;
				// check if the satellite was used in the most recent fix
				if (sat.usedInFix())
				{
					// add the satellite to the number of fix satellites
					nFixed++;
				}
			}
			// log the new GPS status
			Log.d("GPS", "Fixed=" + nFixed + "  Sats=" + nSats);	
		}
		
	};
	
	/**
	 * Called when the GPS Service is started
	 * 
	 * @param   intent - Intent - The Intent supplied to startService(Intent)
	 * @param	flags - int - Additional data about this start request.
	 * @param	startId - int - A unique integer representing this specific request to start
	 * @return	void
	 */
	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		Log.d("GPS", "GPS service started");
		
		// get location manager from the system
		this.locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
		
		// add the GPS status listener to get status updates
		this.locationManager.addGpsStatusListener(this.statusListener);
		// remove updates if any for the location listener
        this.locationManager.removeUpdates(this.locationListener);
        
        // check if the update rate is tight
        if (UPDATE_RATE == TIGHT)
        {
        	// request new updates in tight time
        	this.locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, TIGHT_TIME, MIN_DISTANCE, this.locationListener);
        }
        else
        {
        	// request new updates in loose time
        	this.locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOOSE_TIME, MIN_DISTANCE, this.locationListener);
        }
        // try to get the last known location
        Location location = this.locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        // check if got the last known location
        if (location != null)
        {
        	// make sure last known location is better then the current one
        	if (this.isBetterLocation(location))
    		{
        		// set the current location to last known location
    			this.currentLocation = location;
    		}
        }
        
        // the service should be sticky
		return START_STICKY;
	}
	
	/**
	 * Called when the GPS Service is bound
	 * 
	 * @param   intent - Intent - The Intent that was used to bind to this service
	 * @return	IBinder
	 */
	@Override
	public IBinder onBind(Intent intent) 
	{
		Log.d("GPS", "GPS service bound");
		// get the intent extras
		Bundle data = intent.getExtras();
		// check if got extras from the intent
		if (data != null) 
		{
			// set the out messenger passed in the data bundle
    		this.outMessenger = (Messenger) data.get(Services.MESSENGER);
		}
		
		// Return messenger to this service to the managing CitsciGPS object
		return this.inMessenger.getBinder();
	}
	
	/**
	 * Called when the GPS Service is unbound
	 * 
	 * @param   intent - Intent - The Intent that was used to unbind to this service
	 * @return	boolean - return true so that onRebind will be called
	 */
	@Override
	public boolean onUnbind(Intent intent)
	{
		// start the stop timer to end the service in 5 min
		this.stopHandler.stopIn(5 * 60);
		
		return true;
	}
	
	/**
	 * Called when the GPS Service is rebound
	 * 
	 * @param   intent - Intent - The Intent that was used to rebind to this service
	 * @return	void
	 */
	@Override
	public void onRebind(Intent intent)
	{
		// service has been rebound so cancel the timer
		this.stopHandler.cancel();
	}
	
	/**
	 * Called when the service is destroyed
	 * 
	 * @param   void
	 * @return	void
	 */
	@Override
    public void onDestroy() 
	{
		// check that have location manager
		if (this.locationManager != null)
		{
			// remove location listener from the location manager
			this.locationManager.removeUpdates(this.locationListener);
			// remove GPS status listener from the location manager
	        this.locationManager.removeGpsStatusListener(this.statusListener);
		}
    }
	
	/**
	 * Called on a new location
	 * 
	 * @param   location - Location - the new location
	 * @return	void
	 */
	public void locationUpdate(Location location)
	{
		// check if the new location is better then the old location
		if (this.isBetterLocation(location))
		{
			// log the new location update time
			Log.d("GPS", "New location: " + location.getTime());	
			// set the currentLocation to the new location
			this.currentLocation = location;
			// send the new location to the managing Citsci object
			this.sendLocation();
		}
	}
	
	/**
	 * Send the current location to the managing Citsci GPS object
	 * 
	 * @param   void
	 * @return	void
	 */
	public void sendLocation()
	{
		// create new message data bundle
  		Bundle bundle = new Bundle();
  		// add the current locations data to the bundle
  		bundle.putString(PROVIDER, this.currentLocation.getProvider());
  		bundle.putDouble(LATITUDE, this.currentLocation.getLatitude());
  		bundle.putDouble(LONGITUDE, this.currentLocation.getLongitude());
  		bundle.putDouble(ALTITUDE, this.currentLocation.getAltitude());
  		bundle.putFloat(BEARING, this.currentLocation.getBearing());
  		bundle.putFloat(SPEED, this.currentLocation.getSpeed());
  		bundle.putLong(TIME, this.currentLocation.getTime());
  		bundle.putFloat(ACCURACY, this.currentLocation.getAccuracy());
  		
  		Log.d("GPS", "Sending location from time: " + this.currentLocation.getTime());	
  		// send the location data and a ok result status to the managing Citsci GPS object
  		Services.sendMessage("GPS", outMessenger, Activity.RESULT_OK, bundle);
	}
	
	/**
	 * Check if the new location is better then the current one
	 * 
	 * @param   newLocation - Location - the new location to check against
	 * @return	boolean - whether the new location is better then the old one
	 */
	protected boolean isBetterLocation(Location newLocation) 
	{
		// check that there is a current location
		if (this.currentLocation == null) 
		{
			// the new location is for sure better then the no location
			return true;
		}

		// get the time difference between the two locations
		long timeDelta = newLocation.getTime() - this.currentLocation.getTime();
		// is the location significantly newer
		boolean isSignificantlyNewer;
		// is the location significantly older
		boolean isSignificantlyOlder;
		// check if the update rate is tight
		if (UPDATE_RATE == TIGHT)
		{
			// if the time difference is greater then the tight time
			isSignificantlyNewer = timeDelta > TIGHT_TIME;
			// if the time difference is less then negative the tight time
			isSignificantlyOlder = timeDelta < -TIGHT_TIME;
		}
		else
		{
			// if the time difference is greater then the loose time
			isSignificantlyNewer = timeDelta > LOOSE_TIME;
			// if the time difference is less then negative the loose time
			isSignificantlyOlder = timeDelta < -LOOSE_TIME;
		}
		// if time differnce is positive then the new location is newer then the old one
		boolean isNewer = timeDelta > 0;

		// If it's been more the update rate time then the user may have moved so the new location is better
		if (isSignificantlyNewer) 
		{
			return true;
		} 
		// If it's been more the update rate time older then the current location then it is assumed to be worse
		else if (isSignificantlyOlder) 
		{
			return false;
		}

		// Check the differnce between the new and current location accuracy
		int accuracyDelta = (int) (newLocation.getAccuracy() - this.currentLocation.getAccuracy());
		// the new location is less accurate
		boolean isLessAccurate = accuracyDelta > 0;
		// the new location is more accurate
		boolean isMoreAccurate = accuracyDelta < 0;
		// the new location is significantly less accurate
		boolean isSignificantlyLessAccurate = accuracyDelta > ACCURACY_DELTA;
		
		// the two locations are from the same provider
		boolean isFromSameProvider;
		// check if the new location does not have a provider
		if (newLocation.getProvider() == null)
		{
			// if the current location also does not have a provider
			isFromSameProvider = (this.currentLocation.getProvider() == null);
		}
		else
		{
			// if the provider on the new location is equal to the current locations provider
			isFromSameProvider = newLocation.getProvider().equals(this.currentLocation.getProvider());
		}

		// check if the new location is more accurate
		if (isMoreAccurate) 
		{
			return true;
		} 
		// check if the new location newer and is not less accurate
		else if (isNewer && ! isLessAccurate) 
		{
			return true;
		} 
		// check if the new location newer and is not significantly less accurate and is from the same provider
		else if (isNewer && ! isSignificantlyLessAccurate && isFromSameProvider) 
		{
			return true;
		}
       
		// found no reason to believe the new provider is more better
		return false;
	}

}
